using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Magiq.Support {
    public class WeakDictionary<T, U> : IDictionary<T, U> where U : class {
        private readonly Dictionary<T, Reference<U>> Dictionary = new Dictionary<T, Reference<U>>();
        private readonly int MaxAmountOfItems;

        public WeakDictionary(int maxAmountOfItems) {
            MaxAmountOfItems = maxAmountOfItems;
        }

        public WeakDictionary() : this(20) {}

        #region IDictionary<T,U> Members

        public IEnumerator<KeyValuePair<T, U>> GetEnumerator() {
            return GetEnumerable().GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }

        public void Add(KeyValuePair<T, U> item) {
            Add(item.Key, item.Value);
        }

        public void Clear() {
            Dictionary.Clear();
        }

        bool ICollection<KeyValuePair<T, U>>.Contains(KeyValuePair<T, U> item) {
            return Dictionary.Contains(WeakPairFrom(item));
        }

        public void CopyTo(KeyValuePair<T, U>[] array, int arrayIndex) {
            GetEnumerable().ToList().CopyTo(array, arrayIndex);
        }

        public bool Remove(KeyValuePair<T, U> item) {
            return ((ICollection<KeyValuePair<T, Reference<U>>>) Dictionary).Remove(WeakPairFrom(item));
        }

        public int Count {
            get { return Dictionary.Count; }
        }

        public bool IsReadOnly {
            get { return false; }
        }

        public bool ContainsKey(T key) {
            return Dictionary.ContainsKey(key);
        }

        public void Add(T key, U value) {
            Reference<U> weakValue;
            if (Dictionary.TryGetValue(key, out weakValue)) {
                if (!weakValue.IsAlive)
                    Dictionary.Remove(key);
            }
            MakeWeakTheLessUsedStrongReferenceIfMaxExceeded();
            Dictionary.Add(key, new Reference<U>(value));
        }

        public bool Remove(T key) {
            var result = Dictionary.Remove(key);

            MakeStrongTheMoreUsedWeakReferenceIfMaxIsNotExceeded();

            return result;
        }

        public bool TryGetValue(T key, out U value) {
            Reference<U> weakValue;
            if (Dictionary.TryGetValue(key, out weakValue) && weakValue.IsAlive) {
                value = weakValue.Target;
                return true;
            }
            value = null;
            return false;
        }

        public U this[T key] {
            get {
                var value = Dictionary[key].Target;
                if (value == null)
                    throw new KeyNotFoundException("The key was not found. Maybe it was garbage collected.");

                return value;
            }
            set {
                Remove(key);
                Add(key, value);
            }
        }

        public ICollection<T> Keys {
            get { return GetEnumerable().Select(x => x.Key).ToList(); }
        }

        public ICollection<U> Values {
            get { return GetEnumerable().Select(x => x.Value).ToList(); }
        }

        #endregion

        private IEnumerable<KeyValuePair<T, U>> GetEnumerable() {
            return Dictionary.Where(x => x.Value.IsAlive).Select(x => StrongTypedValueFrom(x));
        }

        private static KeyValuePair<T, U> StrongTypedValueFrom(KeyValuePair<T, Reference<U>> pair) {
            return new KeyValuePair<T, U>(pair.Key, pair.Value.Target);
        }

        private static KeyValuePair<T, Reference<U>> WeakPairFrom(KeyValuePair<T, U> item) {
            return new KeyValuePair<T, Reference<U>>(item.Key, new Reference<U>(item.Value));
        }

        private void MakeWeakTheLessUsedStrongReferenceIfMaxExceeded() {
            var strongReferences = GetStrongReferences();
            if (strongReferences.Count() == MaxAmountOfItems)
                strongReferences.OrderBy(x => x.TimesUsed).First().MakeWeak();
        }

        private IEnumerable<Reference<U>> GetStrongReferences() {
            return Dictionary.Values.Where(x => x.IsStrong);
        }

        private IEnumerable<Reference<U>> GetWeakReferences() {
            return Dictionary.Values.Where(x => x.IsWeak);
        }

        private void MakeStrongTheMoreUsedWeakReferenceIfMaxIsNotExceeded() {
            var strongReferences = GetStrongReferences();
            if (strongReferences.Count() >= MaxAmountOfItems) return;

            var first = GetWeakReferences().OrderByDescending(x => x.TimesUsed).FirstOrDefault();
            if (first != null)
                first.MakeStrong();
        }
    }
}